iT邦幫忙

2025 iThome 鐵人賽

DAY 17
1
Software Development

消除你程式碼的臭味系列 第 17

Day 17- 分離關注點:設定與主要邏輯分開

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250903/20124462P1N8QjGguI.png

消除你程式碼的臭味 Day 17- 分離關注點:設定與主要邏輯分開

把變動頻繁的設定從穩定的主要邏輯裡拿出去。

你的核心商業邏輯,應該像一個在無菌室裡工作的科學家。
它不應該知道、也不應該關心外界的天氣(執行環境)如何。

需要的資料(設定),應該由一個助手(應用程式的啟動層)準備好,然後從門口遞進來。

一個函式或模組的簽名(signature),就是它對外部世界立下的契約(contract)
設定外置,邏輯內聚。

什麼是設定檔?

  • 環境變數、檔案路徑、URL、金鑰、逾時、flag。
  • 與部署環境相關、會因 stage/region 變動的內容。

你的函式,是一份誠實的契約嗎?

每個函式的簽名 (Signature),就是它向外界立下的 契約 (Contract)
一份好的契約,必須是誠實、公開且不含任何隱藏條款的。

拿一個簡單指令 cp 來舉例。
它的用法是: cp <來源檔案> <目標路徑>
這行簡單的文字,就是它與你簽訂的契約。

  • 契約名稱 (Name): cp
  • 契約要求 (Parameters): 你必須提供 來源檔案目標路徑 這兩項資訊。
  • 契約承諾 (Promise): 只要你提供上述資訊,我就保證會執行複製操作,並告訴你結果(成功/失敗)。我不管其他的,我只關心你給我的參數。

這個契約是誠實且公開的,建立了我們對它的信任。
cp 指令非常可靠,它絕不會在你背後偷偷讀取某個全域設定檔,或是根據今天的日期決定要不要多複製一份。
它只做契約上寫明的事,並且信守承諾。

https://ithelp.ithome.com.tw/upload/images/20250919/20124462GfVK2niBKR.png

經典案例:函式裡面直接讀環境

讓一個應該只負責「連線」的函式,同時也承擔了「讀取設定」的職責。

// 🔴 臭味:它的契約隱藏輸入,職責混淆
async function connect() {
  // 它偷偷從外部世界拿了一個全域變數。 
  // 這是一個隱藏的、未在簽名中聲明的依賴。
  const url = process.env.DB_URL;
  return await open(url);
}

不誠實的契約
https://ithelp.ithome.com.tw/upload/images/20250919/20124462AUuDdPFULR.png

  • 它無法被正常測試: 我想測試這個 connect 函式嗎?我現在必須去污染全域的 process.env 物件。這是一種很髒的測試方式。好的測試應該是隔離的,不需要去修改那些全域共享的狀態。

  • 它沒有彈性: 如果想在同一個程式裡,臨時連線到一個不同的測試資料庫怎麼辦?我辦法簡單地傳一個不同的 URL 給 connect。必須去修改整個程式的執行環境。

  • 它不誠實: 它隱藏了「我需要一個資料庫 URL」這個重要的依賴關係。

注入設定:簽訂一份誠實的契約

把依賴關係從隱含的秘密,變成明確的契約條款。

// 🟢 好品味:這是一個誠實的函式。它的契約清晰、完整。

// 這個函式現在很純粹。給它什麼設定,它就用什麼設定。
// 它 100% 遵守了它的簽名契約。
async function connect(dbUrl) {
  return await open(dbUrl);
}

// --- 在應用程式的最外層 (entry point / 組裝處) ---

// 這是唯一一個需要處理「髒活」的地方。
// 它負責從外部世界讀取設定,然後把它們組裝好。
const config = { 
  dbUrl: process.env.DB_URL,
  // ... 其他所有設定
};

// 然後,把乾淨的設定物件,作為契約的一部分,注入到我們誠實的函式中。
connect(config.dbUrl);

誠實的契約
https://ithelp.ithome.com.tw/upload/images/20250919/20124462b1VUP1FDwK.png

  • 誠實的契約: connect(dbUrl) 這個簽名清晰地宣告了它的依賴:「你需要提供我一個 URL,我才能工作。」沒有秘密,沒有隱藏的魔法。

  • 可預測且可測試: 可以輕鬆地測試這個函式:connect('fake_db_url_for_testing')
    不需要碰任何全域變數,測試變得簡單、快速、可靠。

  • 關注點分離: 有個清晰的邊界。
    應用程式的啟動層(最外層)負責處理所有與環境相關的「髒活」。
    應用程式的主要邏輯層,則是完全乾淨、與環境無關的,它們只透過誠實的契約(函式參數)來溝通。

常見模式

  • 依賴注入(Dependency Injection):任何需要設定的函式,都必須把那個設定結構當成參數明確地傳給它,而不是在函式中直接讀環境。

  • 設定層:應用程式啟動的第一件事,就是從環境變數或設定檔裡把所有需要的「資料」讀進來,塞進一個簡單的結構(struct)裡。讀完之後,就再也不要去碰那些外部的東西。

  • 預設值與覆蓋:本地有預設,環境可覆蓋;缺值時清楚報錯。

檢查清單

  1. 函式是否直接讀環境變數或或設定檔?
  2. 設定能否由外部注入?
  3. 是否有集中組裝設定的入口?
  4. 測試能否在完全沒有環境變數的狀況下獨立運行?

今日重點

重點就一個:把會變的東西(資料)和不會變的東西(邏輯)分開。


上一篇
Day 16- 單一職責:找到唯一修改理由,告別脆弱程式碼
下一篇
Day 18- 狀態管理:用資料定義流程
系列文
消除你程式碼的臭味18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言